/*
 * ﻿Copyright (C) 2012-2013 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.aspectjtuil.testutil.junit;

import java.util.Arrays;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.ConstructorSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.aspectj.lang.reflect.SourceLocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Abstract Aspect to dump trace information of code invocation(s) for assisting debugging. <br />
 * Time to time there is a situation requiring debugging in development or test phase. In such situation, 
 * quickly creating a temporally Aspect class by just extending this can automatically log the next 
 * information in trace of various invocations depending on how you override the abstract pointcut 
 * definition (<code>{@link #pointcutForTargetInvocation()}</code>):
 * <ul>
 * <li>Log at start and end of invocation.</li>
 * <li>Arguments to method or constructor.</li>
 * <li>Line number in source</li>
 * <li>Return value of method invocation.</li>
 * <li>Constructed object by constructor invocation.</li>
 * </ul>
 * Such easy usage and flexibility for dumping trace information, especially with AspectJ's LTW (Load 
 * Time Weaving), will become a valuable tool for debugging additionally to our lovable debugger.<br />
 * 
 * <h4>Usage</h4>
 * Extend this (do not forget annotating your sub-class with AspectJ's <code>&#x0040;Aspect</code> 
 * annotation) with following implementation:
 * <ul>
 *  <li>Override <code>{@link #pointcutForTargetInvocation()}</code> abstract pointcut for 
 *  	invocation of what you want to have information.<br />
 *  	Do not forget annotating with AspectJ's <code>&#x0040;Pointcut</code> annotation at override.
 *  </li>
 * </ul>
 * <h4>Example</h4>
 * <ul>
 *  <li>Implementation example<br />
 * <blockquote><code>
 * <b>&#x0040;Aspect</b><br />
 * public static class DebugAssistAspect <b>extends DebugAssistAbstractAspect</b> {<br />
 * &nbsp;&nbsp;<b>&#x0040;Override</b><br />
 * &nbsp;&nbsp;<b>&#x0040;Pointcut</b>( <b>value</b> <br />
 * &nbsp;&nbsp;&nbsp;&nbsp;= "cflow( withincode( public void com.newmainsoftech.aspectjtuil.testutil.junit"<br />
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;".DebugAssistAbstractAspectTest.test*())) " <br />
 * &nbsp;&nbsp;&nbsp;&nbsp;+ "&amp;&amp; (initialization( com.newmainsoftech.aspectjtuil.testutil.junit" <br />
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;".DebugAssistAbstractAspectTest.Debuggee.new( ..)) " <br />
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;+ "|| execution( * com.newmainsoftech.aspectjtuil.testutil.junit" <br />
 * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;".DebugAssistAbstractAspectTest.Debuggee.*( ..)))")<br />
 * &nbsp;&nbsp;<b>public void pointcutForTargetInvocation()</b> {}<br />
 * </code></blockquote>
 *  </li>
 *  <li>Output example<br />
 *  	<blockquote>
 *  	...<br />
 *  	21:17:05.187 [main] INFO  c.n.a.t.j.DebugAssistAbstractAspectTest$DebugAssistAspect -<br />
 *  	Invocation <b>start</b> : com.newmainsoftech.aspectjtuil.testutil.junit.DebugAssistAbstractAspectTest$<b>Debuggee.&lt;init&gt;()</b> &#x0040; DebugAssistAbstractAspectTest.java:238<br />
 *  	21:17:05.187 [main] INFO  c.n.a.t.j.DebugAssistAbstractAspectTest$DebugAssistAspect -<br />
 *  	Invocation <b>start</b> : com.newmainsoftech.aspectjtuil.testutil.junit.DebugAssistAbstractAspectTest$<b>Debuggee.toString()</b> &#x0040; DebugAssistAbstractAspectTest.java:230<br />
 *  	21:17:05.187 [main] INFO  c.n.a.t.j.DebugAssistAbstractAspectTest$DebugAssistAspect -<br />
 *  	Invocation <b>end</b> : com.newmainsoftech.aspectjtuil.testutil.junit.DebugAssistAbstractAspectTest$<b>Debuggee.toString()</b> &#x0040; DebugAssistAbstractAspectTest.java:230<br />
 *  	<b>Returned</b> Debuggee&#x0040;17794055[intField=-1, stringField=stringField, debuggeeList=[], debuggeeArray=null]<br />
 *  	21:17:05.203 [main] INFO  c.n.a.t.j.DebugAssistAbstractAspectTest$DebugAssistAspect -<br />
 *  	Invocation <b>end</b> : com.newmainsoftech.aspectjtuil.testutil.junit.DebugAssistAbstractAspectTest$Debuggee.&lt;init&gt;() &#x0040; DebugAssistAbstractAspectTest.java:238<br />
 *  	<b>Constructed</b> Debuggee&#x0040;17794055[intField=-1, stringField=stringField, debuggeeList=[], debuggeeArray=null] 
 *  	...<br />
 *  	</blockquote>
 *  </li>
 * </ul>
 * 
 * @author Arata Y.
 */
@Aspect
public abstract class DebugAssistAbstractAspect {
	private Logger logger = LoggerFactory.getLogger( this.getClass());
		protected Logger getLogger() {
			return logger;
		}
	
	protected boolean isLooping( 
			final Object obj, final WeakHashMap<Class<?>, AtomicBoolean> loopCheckMap) {
		Class<?> classObj = obj.getClass();
		AtomicBoolean isLooping = loopCheckMap.get( classObj);
			if ( isLooping == null) {
				loopCheckMap.put( classObj, new AtomicBoolean( false));
				return false;
			}
			return isLooping.get();
	}
	protected void setLooping( 
			final Object obj, final WeakHashMap<Class<?>, AtomicBoolean> loopCheckMap, boolean isLooping) {
		Class<?> classObj = obj.getClass();
		AtomicBoolean atomicBoolean = loopCheckMap.get( classObj);
			if ( atomicBoolean == null) {
				loopCheckMap.put( classObj, new AtomicBoolean( isLooping));
			}
			else {
				atomicBoolean.set( isLooping);
			}
	}
		
	/**
	 * @param parameterNames
	 * @param args
	 * @return Dump of given arguments via <code>parameterNames</code> and <code>args</code> 
	 * array arguments. <br />
	 * Empty string when no arguments are given.
	 */
	protected String dumpArgs( final String[] parameterNames, final Object[] args) {
		if ( args.length < 1) return "";
		
		StringBuffer stringBuffer = new StringBuffer();
			for( int index = 0; index < parameterNames.length; index++) {
				if ( index > 0) stringBuffer.append( ",");
				String argName = parameterNames[ index];
					stringBuffer.append( argName);
				Object argValue = args[ index];
					if ( argValue == null) {
						stringBuffer.append( "=null");
					}
					else {
						Class<?> argClass = argValue.getClass();
						stringBuffer.append( "(type:").append( argClass.getSimpleName()).append( ")")
							.append( "=");
							if ( argClass.isArray()) {
								stringBuffer.append( Arrays.toString( (Object[])argValue));
							}
							else {
								stringBuffer.append( argValue.toString());
							}
					}
			}
		return stringBuffer.toString();
	}
	
	/**
	 * Override this abstract pointcut with your concrete pointcut for construction, or method call or 
	 * execution of target.
	 */
	@Pointcut
	public abstract void pointcutForTargetInvocation();
	
	/**
	 * Prefix of log for target invocation incident having cross-cut 
	 * by overridden <code>{@link #pointcutForTargetInvocation()}</code> pointcut.
	 */
	public static final String InvocationStartLogPrefix = String.format( "%nInvocation start : ");
		/**
		 * @return {@value #InvocationStartLogPrefix}
		 */
		public static String getInvocationStartLogPrefix() {
			return DebugAssistAbstractAspect.InvocationStartLogPrefix;
		}
	
	/**
	 * Override this for more control of whether enabling logging target invocation (like method call, 
	 * method execution and/or construction) having cross-cut by overridden 
	 * <code>{@link #pointcutForTargetInvocation()}</code> pointcut.<br /> 
	 * 
	 * @param joinPoint
	 * @return When true, target invocation will be logged by 
	 * <code>{@link #dumpTargetInvocation(JoinPoint)}</code> before-advise.
	 */
	public boolean isInvocationLogged( JoinPoint joinPoint) {
		return true;
	}

	/**
	 * <code>ThreadLocal</code> to control avoiding cross-cut loop by target invocation.
	 */
	protected static ThreadLocal<WeakHashMap<Class<?>, AtomicBoolean>> dumpTargetInvocationLoopCheckMap 
	= new ThreadLocal<WeakHashMap<Class<?>,AtomicBoolean>>();
		static {
			DebugAssistAbstractAspect.dumpTargetInvocationLoopCheckMap
			.set( new WeakHashMap<Class<?>, AtomicBoolean>());
		}
		protected WeakHashMap<Class<?>, AtomicBoolean> getDumpTargetInvocationLoopCheckMap() {
			return DebugAssistAbstractAspect.dumpTargetInvocationLoopCheckMap.get();
		}
		protected boolean isDumpTargetInvocationLooping( Object obj) {
			return isLooping( obj, getDumpTargetInvocationLoopCheckMap());
		}
		protected void setDumpTargetInvocationLooping( Object obj, boolean isDumpTargetInvocationLooping) {
			setLooping( obj, getDumpTargetInvocationLoopCheckMap(), isDumpTargetInvocationLooping);
		}
		
	/**
	 * Logs information (including type, method name, arguments, and line number) of target invocation 
	 * incident (like method call, method execution and/or construction) having cross-cut by overridden 
	 * <code>{@link #pointcutForTargetInvocation()}</code> pointcut.<br />
	 * 
	 * @param joinPoint will be automatically injected by AspectJ.
	 */
	@Before( value="pointcutForTargetInvocation()")
	public void dumpTargetInvocation( JoinPoint joinPoint) {
		Signature signature = joinPoint.getSignature();
		if ( (!isInvocationLogged( joinPoint) || !(signature instanceof MethodSignature))
				&& (!isConstructionLogged( joinPoint) || !(signature instanceof ConstructorSignature))) {
			return;
		}
		
		if ( isDumpTargetInvocationLooping( this)) return;
		setDumpTargetInvocationLooping( this, true);
		
			StringBuffer stringBuffer = new StringBuffer();
				stringBuffer.append( 
						DebugAssistAbstractAspect.getInvocationStartLogPrefix());
					CodeSignature codeSignature = (CodeSignature)signature;
					stringBuffer.append( codeSignature.getDeclaringTypeName()).append( ".")
						.append( codeSignature.getName()).append( "(")
						.append( dumpArgs( codeSignature.getParameterNames(), joinPoint.getArgs()))
						.append( ")");
					SourceLocation sourceLocation = joinPoint.getSourceLocation();
					stringBuffer.append( " @ ").append( sourceLocation.getFileName())
						.append( ":").append( sourceLocation.getLine());
				
			Logger logger = getLogger();
				if ( logger.isInfoEnabled()) {
					logger.info( stringBuffer.toString());
				}
				else if ( logger.isDebugEnabled()) {
					logger.debug( stringBuffer.toString());
				}
				else if ( logger.isTraceEnabled()) {
					logger.trace( stringBuffer.toString());
				}
			
		setDumpTargetInvocationLooping( this, false);
	}
	
	/**
	 * Prefix of log for the end of target invocation incident having cross-cut 
	 * by overridden <code>{@link #pointcutForTargetInvocation()}</code> pointcut.
	 */
	public static final String InvocationEndLogPrefix = String.format( "%nInvocation end : ");
		/**
		 * @return {@value #InvocationEndLogPrefix}
		 */
		public static String getInvocationEndLogPrefix() {
			return DebugAssistAbstractAspect.InvocationEndLogPrefix;
		}
		
	/**
	 * Prefix of log for returned object by target invocation incident having cross-cut 
	 * by overridden <code>{@link #pointcutForTargetInvocation()}</code> pointcut.
	 */
	public static final String ReturningLogPrefix = String.format( "%nReturned ");
		/**
		 * @return {@value #ReturningLogPrefix}
		 */
		public static String getReturningLogPrefix() {
			return ReturningLogPrefix;
		}

	/**
	 * Override this for more control of whether enabling logging returning from target invocation 
	 * (like method call and/or execution) having cross-cut by overridden 
	 * <code>{@link #pointcutForTargetInvocation()}</code> pointcut.<br /> 
	 * 
	 * @param joinPoint
	 * @param returnedObj
	 * @return When true, returning incident from target invocation will be logged.
	 */
	public boolean isReturnLogged( JoinPoint joinPoint, Object returnedObj) {
		return true;
	}
	
	/**
	 * <code>ThreadLocal</code> to control avoiding cross-cut loop by returning from target invocation.
	 */
	protected static ThreadLocal<WeakHashMap<Class<?>, AtomicBoolean>> dumpTargetReturnLoopCheckMap 
	= new ThreadLocal<WeakHashMap<Class<?>,AtomicBoolean>>();
		static {
			DebugAssistAbstractAspect.dumpTargetReturnLoopCheckMap
			.set( new WeakHashMap<Class<?>, AtomicBoolean>());
		}
		protected WeakHashMap<Class<?>, AtomicBoolean> getDumpTargetReturnLoopCheckMap() {
			return dumpTargetReturnLoopCheckMap.get();
		}
		protected boolean isDumpTargetReturnLooping( Object obj) {
			return isLooping( obj, getDumpTargetReturnLoopCheckMap());
		}
		protected void setDumpTargetReturnLooping( Object obj, boolean isLooping) {
			setLooping( obj, getDumpTargetReturnLoopCheckMap(), isLooping);
		}
	
	/**
	 * Logs information (including type, method name, arguments, line number, and returned object) of 
	 * return incident from target invocation (like method call and/or method execution) having 
	 * cross-cut by overridden <code>{@link #pointcutForTargetInvocation()}</code> pointcut.<br />
	 * 
	 * @param joinPoint will be automatically injected by AspectJ.
	 */
	@AfterReturning( pointcut="pointcutForTargetInvocation()", returning="returnedObj")
	public void dumpTargetReturn( JoinPoint joinPoint, Object returnedObj) {
		if ( !isReturnLogged( joinPoint, returnedObj)) return;

		if ( isDumpTargetReturnLooping( this)) return;
		setDumpTargetReturnLooping( this, true);
		
			Signature signature = joinPoint.getSignature();
			if ( signature instanceof CodeSignature) {
				StringBuffer stringBuffer = new StringBuffer();
					stringBuffer.append( DebugAssistAbstractAspect.getInvocationEndLogPrefix());
						CodeSignature codeSignature = (CodeSignature)signature;
						stringBuffer.append( codeSignature.getDeclaringTypeName()).append( ".")
							.append( codeSignature.getName()).append( "(")
							.append( dumpArgs( codeSignature.getParameterNames(), joinPoint.getArgs()))
							.append( ")");
						SourceLocation sourceLocation = joinPoint.getSourceLocation();
						stringBuffer.append( " @ ").append( sourceLocation.getFileName())
							.append( ":").append( sourceLocation.getLine());
					
					if ( signature instanceof MethodSignature) {
						MethodSignature methodSignature = (MethodSignature)signature;
							if ( !Void.class.equals( methodSignature.getReturnType())) {
								stringBuffer.append( 
										DebugAssistAbstractAspect.getReturningLogPrefix());
									if ( returnedObj == null) {
										stringBuffer.append( "null");
									}
									else {
										if ( returnedObj.getClass().isArray()) {
											stringBuffer.append( Arrays.toString( (Object[])returnedObj));
										}
										else {
											stringBuffer.append( returnedObj.toString());
										}
									}
							}
					}
					
				Logger logger = getLogger();
					if ( logger.isInfoEnabled()) {
						logger.info( stringBuffer.toString());
					}
					else if ( logger.isDebugEnabled()) {
						logger.debug( stringBuffer.toString());
					}
					else if ( logger.isTraceEnabled()) {
						logger.trace( stringBuffer.toString());
					}
			}
		
		setDumpTargetReturnLooping( this, false);
	}
	
	/**
	 * Prefix of log for constructed object by target constructor invocation having cross-cut 
	 * by overridden <code>{@link #pointcutForTargetInvocation()}</code> pointcut.
	 */
	public static final String ConstructedLogPrefix = String.format( "%nConstructed ");
		/**
		 * @return {@value #ConstructedLogPrefix}
		 */
		public static String getConstructedLogPrefix() {
			return DebugAssistAbstractAspect.ConstructedLogPrefix;
		}
	/**
	 * Override this for more control of whether enabling logging returning from target constructor 
	 * invocation having cross-cut by overridden <code>{@link #pointcutForTargetInvocation()}</code> 
	 * pointcut.<br />
	 *  
	 * @param joinPoint
	 * @return When true, return from target constructor will be logged.
	 */
	public boolean isConstructionLogged( JoinPoint joinPoint) {
		return false;
	}
	
	/**
	 * <code>ThreadLocal</code> to control avoiding cross-cut loop by returning from 
	 * target constructor invocation.
	 */
	protected static ThreadLocal<WeakHashMap<Class<?>, AtomicBoolean>> dumpTargetConstructionLoopCheckMap 
	= new ThreadLocal<WeakHashMap<Class<?>,AtomicBoolean>>();
		static {
			DebugAssistAbstractAspect.dumpTargetConstructionLoopCheckMap
			.set( new WeakHashMap<Class<?>, AtomicBoolean>());
		}
		protected WeakHashMap<Class<?>, AtomicBoolean> getDumpTargetConstructionLoopCheckMap() {
			return dumpTargetConstructionLoopCheckMap.get();
		}
		protected boolean isDumpTargetConstructionLooping( Object obj) {
			return isLooping( obj, getDumpTargetConstructionLoopCheckMap());
		}
		protected void setDumpTargetConstructionLooping( Object obj, boolean isLooping) {
			setLooping( obj, getDumpTargetConstructionLoopCheckMap(), isLooping);
		}
	
	/**
	 * Logs information (including type, method name, arguments, line number, and constructed object) of 
	 * return incident from target constructor invocation having cross-cut by overridden 
	 * <code>{@link #pointcutForTargetInvocation()}</code> pointcut.<br />
	 * 
	 * @param joinPoint will be automatically injected by AspectJ.
	 */
	@After( value="pointcutForTargetInvocation()")
	public void dumpTargetConstruction( JoinPoint joinPoint) {
		if ( !isConstructionLogged( joinPoint)) return;
		
		if ( isDumpTargetConstructionLooping( this)) return;
		setDumpTargetConstructionLooping( this, true);
		
			Signature signature = joinPoint.getSignature();
			if ( signature instanceof CodeSignature) {
				StringBuffer stringBuffer = new StringBuffer();
					CodeSignature codeSignature = (CodeSignature)signature;
						if ( signature instanceof ConstructorSignature) {
							stringBuffer.append( DebugAssistAbstractAspect.getInvocationEndLogPrefix());
								ConstructorSignature constructorSignature = (ConstructorSignature)signature;
								stringBuffer.append( codeSignature.getDeclaringTypeName()).append( ".")
									.append( codeSignature.getName()).append( "(")
									.append( dumpArgs( codeSignature.getParameterNames(), joinPoint.getArgs()))
									.append( ")");
								SourceLocation sourceLocation = joinPoint.getSourceLocation();
								stringBuffer.append( " @ ").append( sourceLocation.getFileName())
									.append( ":").append( sourceLocation.getLine());
							
							stringBuffer.append( 
									DebugAssistAbstractAspect.getConstructedLogPrefix());
								stringBuffer.append( joinPoint.getTarget().toString());
							
							Logger logger = getLogger();
								if ( logger.isInfoEnabled()) {
									logger.info( stringBuffer.toString());
								}
								else if ( logger.isDebugEnabled()) {
									logger.debug( stringBuffer.toString());
								}
								else if ( logger.isTraceEnabled()) {
									logger.trace( stringBuffer.toString());
								}
						}
			}
		
		setDumpTargetConstructionLooping( this, false);
	}
}

// TODO anonymous method case
